﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Reflection;
using Config.Core.Instructions;

namespace Config.Core
{
    public class XmlParser
    {
        private const int MAX_LOOP_COUNT = 1000;
        private IDictionary<string, string> _props = new Dictionary<string, string>();

        #region Properties
        /// <summary>
        /// Gets or sets the properties.
        /// </summary>
        /// <value>The properties.</value>
        public IDictionary<string, string> Props
        {
            get { return _props; }
            set { _props = value; }
        }

        /// <summary>
        /// Gets or sets the base directory.
        /// </summary>
        /// <value>The base directory.</value>
        public string BaseDirectory { get; set; }
        #endregion


        /// <summary>
        /// Initializes a new instance of the <see cref="XmlParser"/> class.
        /// </summary>
        public XmlParser()
        {
            // Loads the built-in variables.
            _props.Add("builtin.AppDirectory", AppDomain.CurrentDomain.BaseDirectory);
            _props.Add("builtin.CurrentDirectory", Environment.CurrentDirectory);
            _props.Add("builtin.MachineName", Environment.MachineName);
            _props.Add("builtin.OSVersion", Environment.OSVersion.VersionString);
            _props.Add("builtin.SystemDirectory", Environment.SystemDirectory);
            _props.Add("builtin.UserDomainName", Environment.UserDomainName);
            _props.Add("builtin.UserName", Environment.UserName);

            // Loads the environment variables.
            var envVars = Environment.GetEnvironmentVariables();
            foreach (string key in envVars.Keys)
                _props.Add("env." + key, envVars[key].ToString());
        }

        /// <summary>
        /// Parses the specified XML Document.
        /// </summary>
        /// <param name="doc">The XML Document.</param>
        public void Parse(XDocument doc)
        {
            int loopCounter = 0;
            while (true)
            {
                loopCounter++;
                if (loopCounter >= MAX_LOOP_COUNT)
                    throw new Exception("Infinite loop detected!");

                XComment xcomment = 
                    (from comment in doc.DescendantNodes()
                    where comment.NodeType == System.Xml.XmlNodeType.Comment
                        && ((XComment)comment).Value.Trim().ToLower().StartsWith("#config#")
                    select ((XComment)comment)).FirstOrDefault();

                if (xcomment == null)
                    break;

                IInstruction inst = GetInstruction(xcomment);
                inst.Run(doc);
            }
        }

        /// <summary>
        /// Gets the instruction.
        /// </summary>
        /// <param name="comment">The comment.</param>
        /// <returns>The XML Instruction.</returns>
        public IInstruction GetInstruction(XComment comment)
        {
            var dict = new Dictionary<string, string>();
            string instName = Parse(comment, dict);

            IInstruction inst;
            switch (instName)
            {
                case "set":
                    inst = new SetInstruction();
                    break;
                case "insert":
                    inst = new InsertInstruction();
                    break;
                case "delete":
                    inst = new DeleteInstruction();
                    break;
                default:
                    throw new InvalidOperationException("Unrecognised instruction type!");
            }

            PropertyInfo[] fields = inst.GetType().GetProperties();
            foreach (var field in fields)
            {
                object[] attribs = field.GetCustomAttributes(typeof(InstructionParamAttribute), false);
                if (attribs.Length > 0)
                {
                    var param = (InstructionParamAttribute)attribs[0];
                    if (!param.IsOptional && !dict.ContainsKey(param.Name))
                        throw new ArgumentException(String.Format("Missing parameter: {0}", param.Name));

                    if(dict.ContainsKey(param.Name))
                        field.SetValue(inst, dict[param.Name], null);
                }
            }

            inst.XComment = comment;
            return inst;
        }

        /// <summary>
        /// Parses the specified comment.
        /// </summary>
        /// <param name="comment">The comment.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns>The instructio type</returns>
        private string Parse(XComment comment, IDictionary<string, string> dict)
        {
            string inst = Regex.Replace(comment.Value, "#Config#", "", RegexOptions.IgnoreCase).Trim();
            string instName = inst.Substring(0, Regex.Replace(inst, "\n", " ").IndexOf(" ")).ToLower();
            inst = inst.Substring(instName.Length, inst.Length - instName.Length);

            string pattern = String.Format(@"(?<name>.*?)={0}(?<val>.*?){0}", Regex.Escape("\""));
            MatchCollection matches = Regex.Matches(inst, pattern);

            foreach (Match match in matches)
            {
                string key = match.Groups["name"].Value.Trim();
                string val = match.Groups["val"].Value;
                foreach (string propKey in Props.Keys)
                    val = Regex.Replace(val, Regex.Escape("$(" + propKey + ")"), Props[propKey], RegexOptions.IgnoreCase);

                dict.Add(key, val);
            }

            return instName;
        }
    }
}
